package com.tencent.sr.iris.activity.common.util;


import cn.hutool.core.util.StrUtil;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.tencent.sr.iris.activity.common.config.QuFaConfig;
import com.tencent.sr.iris.activity.common.config.ThirdReqSentinelSwitch;
import com.tencent.sr.iris.activity.common.enums.IrisErrorCode;
import com.tencent.sr.rmall.common.exception.code.BaseErrorCode;
import com.tencent.sr.rmall.common.exception.retail.TRetailBizException;
import com.tencent.sr.rmall.common.exception.retail.TRetailErrorException;
import com.tencent.sr.rmall.common.exception.retail.TRetailRpcResultException;
import com.tencent.sr.rmall.common.request.PaginationRequest;
import com.tencent.sr.rmall.common.response.PaginationResponse;
import com.tencent.sr.rmall.springbootstarter.monitor.util.MonitorUtil;
import com.tencent.sr.rmall.springbootstarter.utils.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.*;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * tencentcloud网关请求工具
 */
@Slf4j
@Component
public class QuFaHttpUtilOkHttpClient {

    private static final String MAC_NAME = "HmacSHA1";
    private static final String ENCODING = "UTF-8";
    private static final ConnectionPool POOL_CONFIG;
    private static OkHttpClient okHttpClient;

    static {
        POOL_CONFIG = new ConnectionPool(20, 10L, TimeUnit.MINUTES);
        okHttpClient = (new OkHttpClient.Builder()).connectTimeout(15L, TimeUnit.SECONDS)
                .writeTimeout(10L, TimeUnit.SECONDS).readTimeout(20L, TimeUnit.SECONDS).connectionPool(POOL_CONFIG)
                .build();
    }

    @Resource
    private QuFaConfig config;
    @Resource
    private ThirdReqSentinelSwitch thirdReqSentinelSwitch;
    /**
     * 区发不可派奖错误码，此错误码不需要重试发奖
     */
    private static List<Integer> UN_RETRY_ERROR_CODE = Arrays.asList(50000212, 50000213);

    public <T> PaginationResponse<T> postPageList(String path, Object body, Class<T> tClass) {
        JSONObject jsonRes = postCommon(config, path, JSON.toJSONString(body));
        JSONArray jsonData = jsonRes.getJSONObject("data").getJSONArray("data");
        List<T> dataList = JSON.parseArray(jsonData.toJSONString(), tClass);
        PaginationResponse<T> pageRes = new PaginationResponse<>();
        pageRes.setDataList(dataList);
        pageRes.setPageNum(jsonRes.getJSONObject("data").getInteger("current_page"));
        pageRes.setPageSize(jsonRes.getJSONObject("data").getInteger("per_page"));
        pageRes.setTotalCount(jsonRes.getJSONObject("data").getInteger("total"));
        return pageRes;
    }

    public <REQ> void postObjectNoResult(String path, REQ req) {
        String reqStr = JSON.toJSONString(req);
        JSONObject jsonObject = postCommon(config, path, reqStr);
        log.info("QuFaHttpUtil.postObjectNoResult :{}、{}、{}", path, reqStr, jsonObject);
    }

    public <REQ, RES> RES postObject(String path, REQ req, Class<RES> tClass) {
        JSONObject jsonRes = postCommon(config, path, JSON.toJSONString(req));
        JSONObject jsonData = jsonRes.getJSONObject("data");
        return JSON.parseObject(jsonData.toJSONString(), tClass);
    }

    public <REQ, RES> List<RES> postList(String path, REQ req, Class<RES> tClass) {
        JSONObject jsonRes = postCommon(config, path, JSON.toJSONString(req));
        JSONArray jsonData = jsonRes.getJSONArray("data");
        return JSON.parseArray(jsonData.toJSONString(), tClass);
    }

    public <REQ, RES> PaginationResponse<RES> postPage(String path,
            PaginationRequest<REQ> req, Class<RES> tClass) {
        JSONObject jsonObject = null;
        if (Objects.isNull(req.getQueryCriteria())) {
            jsonObject = new JSONObject();
        } else {
            jsonObject = (JSONObject) JSONObject.toJSON(req.getQueryCriteria());
        }
        jsonObject.put("page", req.getPageNum());
        jsonObject.put("per_page", req.getPageSize());

        JSONObject jsonRes = postCommon(config, path, jsonObject.toJSONString());
        PaginationResponse<RES> pageRes = new PaginationResponse<>();
        pageRes.setPageNum(jsonRes.getJSONObject("data").getInteger("current_page"));
        pageRes.setPageSize(jsonRes.getJSONObject("data").getInteger("per_page"));
        pageRes.setTotalCount(jsonRes.getJSONObject("data").getInteger("total"));
        JSONArray jsonData = jsonRes.getJSONObject("data").getJSONArray("data");
        if (Objects.nonNull(jsonData)) {
            List<RES> dataList = JSON.parseArray(jsonData.toJSONString(), tClass);
            pageRes.setDataList(dataList);
        }
        return pageRes;
    }

    public JSONObject postCommon(QuFaConfig config, String path, String reqBody) {
        long startTime = System.currentTimeMillis();
        Throwable throwable = null;
        try {
            if (thirdReqSentinelSwitch.isSwitchOn()) {
                SentinelUtil.checkSentinelLimit(path);
            }
            log.info("QuFaHttpUtil post url:{},body:{}", config.getHost() + path, reqBody);
            String contentType = "application/json";
            String xDate = getGMTTime();
            String contentMD5 = base64Encode(getMD5(reqBody).getBytes());

            URL parsedUrl = new URL(config.getHost() + path);
            String pathAndParams = parsedUrl.getPath();
            if (parsedUrl.getQuery() != null) {
                pathAndParams = pathAndParams + "?" + sortQueryParams(parsedUrl.getQuery());
            }
            String stringToSign = String.format("x-date: %s\n%s\n%s\n%s\n%s\n%s",
                    xDate, HttpMethod.POST.name(), contentType, contentType, contentMD5, pathAndParams);
            byte[] hmacStr = hmacSHA1Encrypt(stringToSign, config.getAppSecret());
            String signature = base64Encode(hmacStr);
            String authHeader = String.format("hmac id=\"%s\", algorithm=\"hmac-sha1\", "
                    + "headers=\"x-date\", signature=\"%s\"", config.getAppKey(), signature);

            RequestBody requestBody = FormBody.create(MediaType.parse("Content-Type=text/plain;charset=utf-8"),
                    reqBody);
            Request request = new Request.Builder()
                    .url(config.getHost() + path)
                    .post(requestBody)
                    .addHeader("Authorization", authHeader)
                    .addHeader("Accept", contentType)
                    .addHeader("Host", config.getHost()
                            .replace("http://", "").replace("https://", ""))
                    .addHeader("x-date", xDate)
                    .addHeader("Content-Type", contentType)
                    .build();

            Response okHttpResponse = okHttpClient.newCall(request).execute();
            if (okHttpResponse == null) {
                log.error("QuFaHttpUtil post error,path:{},reqBody:{},result:{}", path, reqBody,
                        null);
                throw new TRetailBizException(BaseErrorCode.REMOTE_CALL_LOST);
            }
            int code = okHttpResponse.code();
            if (!okHttpResponse.isSuccessful()) {
                log.error("QuFaHttpUtil post error,code:{},path:{},reqBody:{},result:{}", code, path, reqBody,
                        okHttpResponse);
                throw new TRetailBizException(BaseErrorCode.REMOTE_CALL_LOST, "response code:" + code);
            }
            String responseBody = okHttpResponse.body().string();
            JSONObject jsonRes = JsonUtils.parse(responseBody, JSONObject.class);
            Integer qufaCode = jsonRes.getInteger("code");
            if (qufaCode == null) {
                log.error("QuFaHttpUtil okhttp post error,path:{},requestBody:{},result:{}", path, reqBody,
                        responseBody);
                String error = String.format("请求区发错误，返回：%s，请求参数：%s",
                        jsonRes.getString("msg"), StrUtil.subWithLength(Optional.ofNullable(reqBody).orElse(StrUtil.EMPTY), 0, 500));
                throw new TRetailBizException(IrisErrorCode.REQUEST_QUFA_FAIL, error);
            } else if (qufaCode != 200) {
                String error = String.format("请求区发错误，返回：%s，请求参数：%s",
                        jsonRes.getString("msg"), StrUtil.subWithLength(Optional.ofNullable(reqBody).orElse(StrUtil.EMPTY), 0, 500));
                if (UN_RETRY_ERROR_CODE.contains(qufaCode)) {
                    log.error("QuFaHttpUtil okhttp post error,path:{},requestBody:{},result:{}", path, reqBody,
                            responseBody);
                    throw new TRetailBizException(IrisErrorCode.REQUEST_QUFA_FAIL_UN_RETRY, error);
                }
                log.error("QuFaHttpUtil okhttp post error,path:{},requestBody:{},result:{}", path, reqBody,
                        responseBody);
                throw new TRetailBizException(IrisErrorCode.REQUEST_QUFA_FAIL, error);
            }

            log.info("QuFaHttpUtil okhttp post success,path:{},request:{},result:{}", path, reqBody, responseBody);
            return jsonRes;
        } catch (BlockException ex) {
            log.error("远程调用触发限流,path:{}" + path);
            throw new TRetailErrorException("三方触发限流");
            // 资源访问阻止，被限流或被降级
            // 进行相应的处理操作
        } catch (Exception e) {
            throwable = e;
            if (e instanceof TRetailBizException) {
                TRetailBizException exp = (TRetailBizException) e;
                throw new TRetailBizException(exp.getErrorCode(), exp.getMessage());
            }
            log.error("QuFaHttpUtil post error,path:{},reqBody:{}", path, reqBody, e);
            throw new TRetailRpcResultException(IrisErrorCode.REQUEST_QUFA_FAIL);
        } finally {
            MonitorUtil.getSingleMonitorService().logTransaction("HttpCall", path,
                    startTime, throwable);
        }
    }


    private String getGMTTime() {
        Calendar cd = Calendar.getInstance();
        SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US);
        sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
        return sdf.format(cd.getTime());
    }

    private String sortQueryParams(String queryParam) {
        if (StringUtils.isEmpty(queryParam)) {
            return "";
        }

        Map<String, String> queryPairs = new TreeMap<>();
        String[] queryParams = queryParam.split("&");

        for (String query : queryParams) {
            String[] kv = query.split("=");
            queryPairs.put(kv[0], kv[1]);
        }

        StringBuilder sortedParamsBuilder = new StringBuilder();
        for (Map.Entry<String, String> entry : queryPairs.entrySet()) {
            sortedParamsBuilder.append(entry.getKey());
            sortedParamsBuilder.append("=");
            sortedParamsBuilder.append(entry.getValue());
            sortedParamsBuilder.append("&");
        }
        String sortedParams = sortedParamsBuilder.toString();
        sortedParams = sortedParams.substring(0, sortedParams.length() - 1);

        return sortedParams;
    }

    private byte[] hmacSHA1Encrypt(String encryptText, String encryptKey) throws Exception {
        SecretKey secretKey = new SecretKeySpec(encryptKey.getBytes(ENCODING), MAC_NAME);
        Mac mac = Mac.getInstance(MAC_NAME);
        mac.init(secretKey);

        byte[] text = encryptText.getBytes(ENCODING);
        return mac.doFinal(text);
    }

    private String base64Encode(byte[] key) {
        final Base64.Encoder encoder = Base64.getEncoder();
        return encoder.encodeToString(key);
    }

    private String getMD5(String str) {
        return DigestUtils.md5Hex(str);
    }
}
